﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Owin.Helpers;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security;
using Microsoft.Owin;
using Newtonsoft.Json.Linq;

namespace JY.Owin.Security.Yahoo
{
    internal class YahooAuthenticationHandler : AuthenticationHandler<YahooAuthenticationOptions>
    {
        private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        private const string StateCookie = "__YahooState";
        private const string RequestTokenEndpoint = "https://api.login.yahoo.com/oauth/v2/get_request_token";
        private const string TokenEndpoint = "https://api.login.yahoo.com/oauth/v2/get_token";
        private const string AuthorizeEndpoint = "https://api.login.yahoo.com/oauth/v2/request_auth";
        //private const string ProfileEndpoint = "https://social.yahooapis.com/v1/user/{0}/profile/tinyusercard?format=json";

        private readonly HttpClient _httpClient;
        private readonly ILogger _logger;

        public YahooAuthenticationHandler(HttpClient httpClient, ILogger logger)
        {
            _httpClient = httpClient;
            _logger = logger;
        }

        public override async Task<bool> InvokeAsync()
        {
            if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
            {
                return await InvokeReturnPathAsync();
            }
            return false;
        }

        protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
        {
            AuthenticationProperties properties = null;
            try
            {
                IReadableStringCollection query = Request.Query;
                string protectedRequestToken = Request.Cookies[StateCookie];

                properties = Options.StateDataFormat.Unprotect(protectedRequestToken);

                string cookieReqToken = properties.Dictionary[".request_token"];
                properties.Dictionary.Remove(".request_token");
                string cookieReqSecert = properties.Dictionary[".request_secert"];
                properties.Dictionary.Remove(".request_secert");

                if (properties == null)
                {
                    _logger.WriteWarning("Invalid state");
                    return null;
                }

                string returnedToken = query.Get("oauth_token");
                if (string.IsNullOrWhiteSpace(returnedToken))
                {
                    _logger.WriteWarning("Missing oauth_token");
                    return new AuthenticationTicket(null, properties);
                }

                if (returnedToken != cookieReqToken)
                {
                    _logger.WriteWarning("Unmatched token");
                    return new AuthenticationTicket(null, properties);
                }

                string oauthVerifier = query.Get("oauth_verifier");
                if (string.IsNullOrWhiteSpace(oauthVerifier))
                {
                    _logger.WriteWarning("Missing or blank oauth_verifier");
                    return new AuthenticationTicket(null, properties);
                }


                var authorizationBaseData = new List<KeyValuePair<string, string>>();
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_consumer_key", Options.ConsumerKey));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_nonce", Guid.NewGuid().ToString("N")));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_signature_method", "HMAC-SHA1"));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_timestamp", GenerateTimeStamp()));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_token", returnedToken));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_verifier", oauthVerifier));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_version", "1.0"));

                HttpResponseMessage graphResponse = await _httpClient.SendAsync(GenerateRequestMessage(TokenEndpoint, Options.ConsumerSecret, cookieReqSecert, authorizationBaseData), Request.CallCancelled);
                graphResponse.EnsureSuccessStatusCode();
                string responseText = await graphResponse.Content.ReadAsStringAsync();
                IFormCollection responseParameters = WebHelpers.ParseForm(responseText);

                string userID = responseParameters.Get("xoauth_yahoo_guid");
                string accessToken = responseParameters.Get("oauth_token");
                string accessSecert = responseParameters.Get("oauth_token_secret");

                //var profileReqData = new List<KeyValuePair<string, string>>();
                //profileReqData.Add(new KeyValuePair<string, string>("oauth_consumer_key", Options.ConsumerKey));
                //profileReqData.Add(new KeyValuePair<string, string>("oauth_nonce", Guid.NewGuid().ToString("N")));
                //profileReqData.Add(new KeyValuePair<string, string>("oauth_signature_method", "HMAC-SHA1"));
                //profileReqData.Add(new KeyValuePair<string, string>("oauth_timestamp", GenerateTimeStamp()));
                //profileReqData.Add(new KeyValuePair<string, string>("oauth_token", accessToken));
                //profileReqData.Add(new KeyValuePair<string, string>("oauth_version", "1.0"));

                //string profileEndpoint = string.Format(ProfileEndpoint, userID);
                //graphResponse = await _httpClient.SendAsync(GenerateRequestMessage(profileEndpoint, Options.ConsumerSecret, accessSecert, profileReqData, HttpMethod.Get), Request.CallCancelled);
                //graphResponse.EnsureSuccessStatusCode();

                //responseText = await graphResponse.Content.ReadAsStringAsync();
                //JObject user = JObject.Parse(responseText);

                var context = new YahooAuthenticatedContext(Context, userID, accessToken, accessSecert);

                context.Identity = new ClaimsIdentity(
                    new[]
                    {
                        new Claim(ClaimTypes.NameIdentifier, userID, ClaimValueTypes.String, Options.AuthenticationType),
                        new Claim(ClaimTypes.Name, userID, ClaimValueTypes.String, Options.AuthenticationType),
                        new Claim("urn:Yahoo:userid", userID, ClaimValueTypes.String, Options.AuthenticationType),
                    },
                    Options.AuthenticationType,
                    ClaimsIdentity.DefaultNameClaimType,
                    ClaimsIdentity.DefaultRoleClaimType);
                context.Properties = properties;

                Response.Cookies.Delete(StateCookie);

                await Options.Provider.Authenticated(context);

                return new AuthenticationTicket(context.Identity, context.Properties);
            }
            catch (Exception ex)
            {
                _logger.WriteError("Authentication failed", ex);
                return new AuthenticationTicket(null, properties);
            }
        }

        protected override async Task ApplyResponseChallengeAsync()
        {
            if (Response.StatusCode != 401)
            {
                return;
            }

            AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);

            if (challenge != null)
            {
                string requestPrefix = Request.Scheme + "://" + Request.Host;
                string callBackUrl = requestPrefix + RequestPathBase + Options.CallbackPath;

                AuthenticationProperties extra = challenge.Properties;
                if (string.IsNullOrEmpty(extra.RedirectUri))
                {
                    extra.RedirectUri = requestPrefix + Request.PathBase + Request.Path + Request.QueryString;
                }

                string YahooAuthenticationEndpoint = await ObtainRequestTokenAsync(callBackUrl, extra);

                if (!string.IsNullOrWhiteSpace(YahooAuthenticationEndpoint))
                {
                    var cookieOptions = new CookieOptions
                    {
                        HttpOnly = true,
                        Secure = Request.IsSecure
                    };

                    Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(extra), cookieOptions);

                    var redirectContext = new YahooApplyRedirectContext(
                        Context, Options,
                        extra, YahooAuthenticationEndpoint);
                    Options.Provider.ApplyRedirect(redirectContext);
                }
                else
                {
                    _logger.WriteError("requestToken not get.");
                }
            }
        }

        public async Task<bool> InvokeReturnPathAsync()
        {
            AuthenticationTicket model = await AuthenticateAsync();
            if (model == null)
            {
                _logger.WriteWarning("Invalid return state, unable to redirect.");
                Response.StatusCode = 500;
                return true;
            }

            var context = new YahooReturnEndpointContext(Context, model)
            {
                SignInAsAuthenticationType = Options.SignInAsAuthenticationType,
                RedirectUri = model.Properties.RedirectUri
            };
            model.Properties.RedirectUri = null;

            await Options.Provider.ReturnEndpoint(context);

            if (context.SignInAsAuthenticationType != null && context.Identity != null)
            {
                ClaimsIdentity signInIdentity = context.Identity;
                if (!string.Equals(signInIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
                {
                    signInIdentity = new ClaimsIdentity(signInIdentity.Claims, context.SignInAsAuthenticationType, signInIdentity.NameClaimType, signInIdentity.RoleClaimType);
                }
                Context.Authentication.SignIn(context.Properties, signInIdentity);
            }

            if (!context.IsRequestCompleted && context.RedirectUri != null)
            {
                if (context.Identity == null)
                {
                    // add a redirect hint that sign-in failed in some way
                    context.RedirectUri = WebUtilities.AddQueryString(context.RedirectUri, "error", "access_denied");
                }
                Response.Redirect(context.RedirectUri);
                context.RequestCompleted();
            }

            return context.IsRequestCompleted;
        }

        private async Task<string> ObtainRequestTokenAsync(string callBackUri, AuthenticationProperties properties)
        {
            _logger.WriteVerbose("ObtainRequestToken");
            try
            {
                var authorizationBaseData = new List<KeyValuePair<string, string>>();
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_callback", callBackUri));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_consumer_key", Options.ConsumerKey));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_nonce", Guid.NewGuid().ToString("N")));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_signature_method", "HMAC-SHA1"));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_timestamp", GenerateTimeStamp()));
                authorizationBaseData.Add(new KeyValuePair<string, string>("oauth_version", "1.0"));

                HttpResponseMessage graphResponse = await _httpClient.SendAsync(GenerateRequestMessage(RequestTokenEndpoint, Options.ConsumerSecret, null, authorizationBaseData), Request.CallCancelled);
                graphResponse.EnsureSuccessStatusCode();

                string responseText = await graphResponse.Content.ReadAsStringAsync();

                IFormCollection responseParameters = WebHelpers.ParseForm(responseText);

                string reqToken = responseParameters.Get("oauth_token");
                string reqSecert = responseParameters.Get("oauth_token_secret");
                if (string.IsNullOrWhiteSpace(reqToken) || string.IsNullOrWhiteSpace(reqSecert))
                {
                    return "";
                }

                properties.Dictionary.Add(".request_token", reqToken);
                properties.Dictionary.Add(".request_secert", reqSecert);

                string authUrl = responseParameters.Get("xoauth_request_auth_url");
                if (string.IsNullOrWhiteSpace(authUrl))
                {
                    return string.Format("{0}?oauth_token={1}", AuthorizeEndpoint, reqToken);
                }
                else
                {
                    return authUrl;
                }
            }
            catch (Exception ex)
            {
                _logger.WriteError("not get request token", ex);
                return "";
            }
        }

        private static string GenerateTimeStamp()
        {
            return (DateTime.UtcNow - Epoch).TotalSeconds.ToString("##0");
        }

        private static HttpRequestMessage GenerateRequestMessage(string endpoint, string consumerSecret, string tokenSecret, List<KeyValuePair<string, string>> baseData, HttpMethod method = null)
        {

            var reqString = new StringBuilder();
            foreach (var item in baseData)
            {
                reqString.AppendFormat("{0}={1}&", Uri.EscapeDataString(item.Key), Uri.EscapeDataString(item.Value));
            }
            reqString.Length--;
            string parameterString = reqString.ToString();

            var baseString = new StringBuilder();
            baseString.Append((method ?? HttpMethod.Post).Method);
            baseString.Append("&");
            baseString.Append(Uri.EscapeDataString(endpoint));
            baseString.Append("&");
            baseString.Append(Uri.EscapeDataString(parameterString));

            string signature = ComputeSignature(consumerSecret, tokenSecret, baseString.ToString());
            baseData.Add(new KeyValuePair<string, string>("oauth_signature", signature));

            var headerBuilder = new StringBuilder();
            headerBuilder.Append("OAuth ");

            string GenQuestr = "";

            foreach (var authorizationPart in baseData)
            {
                headerBuilder.AppendFormat(
                    "{0}=\"{1}\", ", authorizationPart.Key, Uri.EscapeDataString(authorizationPart.Value));

                GenQuestr += string.Format("{0}={1}&", authorizationPart.Key, Uri.EscapeDataString(authorizationPart.Value));
            }
            headerBuilder.Length = headerBuilder.Length - 2;

            var request = new HttpRequestMessage(method ?? HttpMethod.Post, endpoint);
            request.Headers.Add("Authorization", headerBuilder.ToString());

            return request;
        }

        private static string ComputeSignature(string consumerSecret, string tokenSecret, string signatureData)
        {
            using (var algorithm = new HMACSHA1())
            {
                algorithm.Key = Encoding.ASCII.GetBytes(
                    string.Format(CultureInfo.InvariantCulture,
                        "{0}&{1}",
                          Uri.EscapeDataString(consumerSecret),
                        string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret)));
                byte[] hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData));
                return Convert.ToBase64String(hash);
            }
        }
    }
}
